aws:PrincipalIsAWSService を使用せずにサービスプリンシパルからのアクセス制御を試してみた(ができなかった)
コンバンハ、千葉(幸)です。
先日のアップデートにより、AWS グローバル条件コンテキストキーaws:PrincipalIsAWSService
が追加されました。
これは「サービスプリンシパルから AWS リソースへのアクセス」に必要なパーミッションの管理をシンプルにしてくれるものであり、例えば以下を同時に満たす S3 バケットポリシーを簡単に定義できます。
- ユーザーからのアクセスは VPC エンドポイント経由に限定したい
- CloudTrail からのアクセスを許可したい
ここで気になるのは、aws:PrincipalIsAWSService
により実現できるようになったのが以下のいずれかということです。
- 今までできなかった管理の仕方ができるようになった
- 今までは複雑なポリシーを書く必要があったがシンプルに書けるようになった
アップデートを取り上げた以下エントリでもそこに触れており、その時点では「aws:PrincipalIsAWSService
を使わずに同様の制御を実現する」ことはできませんでした。
過去がどうであろうと今後はaws:PrincipalIsAWSService
を使えばいいわけですから、そこにこだわる必要はありません。が、こまかいことが気になると夜しかねむれなくなる自分としては、ネチネチとそこを考え続けていました。
そんな中aws:PrincipalType
という 条件キーを見つけました。リクエストを行うプリンシパルのタイプを表すものです。何かそれっぽいことできそう……!と思い、これを使ってのアクセス制御を試してみました。
先にまとめ
- 条件キー
aws:PrincipalType
が取りうる値は以下のいずれか- Account
- User
- FederatedUser
- AssumedRole
- Anonymous
- 状況証拠的に、サービスプリンシパルからのリクエストに含まれる
aws:PrincipalType
キーの値は AssumedRole - 「サービスプリンシパルからのアクセスだけ Allow / Deny する」を
aws:PrincipalType
を使用して実現することはできない
平たく言うと、今回やりたいことは実現できませんでした。でもちょっと面白いなと思う箇所があったので、暇な方は続きを読んでください。
やりたいこと
aws:PrincipalIsAWSService
のドキュメントに記載されている以下のポリシーを、aws:PrincipalType
を使用したものに置き換えたいです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Expected-network+service-principal", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-logs-bucket/AWSLogs/AccountNumber/*", "Condition": { "StringNotEqualsIfExists": { "aws:SourceVpc": "vpc-111bbb22" }, "BoolIfExists": { "aws:PrincipalIsAWSService": "false" } } } ] }
このポリシーは S3 バケットポリシー(の一部)を表しており、冒頭にのせた例と同じく以下を満たすためのものです。
- ユーザーからのアクセスは特定の VPC の VPC エンドポイント経由に限定したい
- CloudTrail からのアクセスを許可したい
前提知識
今回のエントリを読んでいただく上で必要となる知識をおさらいしておきます。
プリンシパル
プリンシパルは AWS リソースへのアクションを行う主体を表します。ドキュメントでは以下のように記載されています。
プリンシパルは、AWS リソースのアクションまたはオペレーションに対してリクエストできるユーザーまたはアプリケーションを指します。
IAM ポリシーにおけるPrincipal
要素の中では、以下のいずれかを指定できます。
- AWS アカウントと ルートユーザー
- IAM ユーザー
- フェデレーティッドユーザー(ウェブ ID または SAML フェデレーションを使用)
- IAM ロール
- ロールを引き受けるセッション
- AWS サービス
- 匿名ユーザー(非推奨)
プリンシパルとなる AWS サービスはサービスプリンシパルと呼ばれます。
リクエストコンテキスト
プリンシパルからのリクエストには、以下のような様々な情報が含まれます。(ドキュメントより引用)
- アクションまたはオペレーション – プリンシパルが実行するアクションまたはオペレーション。AWS マネジメントコンソール ではアクション、AWS CLI や AWS API ではオペレーションです。
- リソース – アクションまたはオペレーションを実行する対象の AWS リソースオブジェクト。
- プリンシパル – エンティティ (ユーザーまたはロール) を使用してリクエストを送信するユーザーまたはアプリケーション。プリンシパルに関する情報には、プリンシパルがサインインに使用したエンティティに関連付けられたポリシーが含まれます。
- 環境データ – IP アドレス、ユーザーエージェント、SSL 有効化ステータス、または時刻に関する情報。
- リソースデータ – リクエストされているリソースに関連するデータ。これには、DynamoDB テーブル名、Amazon EC2 インスタンスのタグなどの情報が含まれる場合があります。
これらのリクエストの内容はリクエストコンテキストに収集され、リクエストの評価と認可に使用されます。
Condition 要素の条件キー
IAM ポリシーにConditon
要素を含めることができます。「リクエストコンテキストに収集されたキーと値の組み合わせ」と「Condition
要素内の条件キーと値の組み合わせ」を突き合わせ、リクエストに対する評価/認可が行われます。
条件キーには以下の2種類があります。
- グローバル条件キー
aws:CurrentTime
aws:referer
aws:PrincipalIsAWSService
など
- サービス固有の条件キー
s3:TlsVersion
ec2:InstanceType
rds:DatabaseEngine
など
前者のグローバル条件キーは特定のサービスに依存しないもので、プレフィックスがaws:
です。以下に一覧があります。
後者のサービス固有の条件キーは該当するサービスへのリクエスト時にのみ含まれるもので、プレフィックスは各サービス名となっています。以下ページから各サービスの詳細ページに遷移することで確認できます。
グローバル条件コンテキストキーの可用性
サービスに依存しないグローバル条件コンテキストキーであっても、すべてのリクエストに含まれているわけではありません。
どういった場合に含まれているかは、以下ページの可用性(Availability)部から確認できます。
今回登場するグローバル条件キーの可用性については以下の通りです。
条件キー | 可用性(概要) |
---|---|
aws:SourceVpc | VPCエンドポイント経由のリクエストにのみ含まれる |
aws:PrincipalIsAWSService | すべての署名付きAPIリクエストに含まれる |
aws:PrincipalType | すべてのリクエストコンテキストに含まれる |
評価ロジック
「今回やりたいこと」で引用したポリシーをもとに、評価ロジックを確認します。
トピックをかいつまんで書くと以下の通りです。
Conditon
要素全体が ture の場合にEffect
要素の Allow / Deny の効果がトリガーされるCondition
ブロック内の各要素は AND 条件で評価される- つまりすべての要素が true の場合のみ全体として true
- IfExists 条件演算子 が付与された場合、リクエストコンテキストにキーが含まれていない場合は true の評価となる
簡単にパターンを想定すると以下の結果となります。
リクエストのパターン | ①部 | ②部 | 全体の結果 |
---|---|---|---|
指定されたVPCエンドポイント経由の場合 | false | true | false |
サービスプリンシパルの場合 | true | false | false |
インターネット経由のIAMユーザーの場合 | true | true | true |
Conditon
要素全体として false であれば Deny 効果は発揮されない、ということになります。
Not
を含む文字列条件演算子や「値が false の Bool条件演算子」が含まれているとこんがらがりますね。
aws:PrincipalType を使用したバケットポリシーの作成
前提をおさらいしたところで、早速本題に入っていきます。
バケットchibayuki-from-vpce
を使用し、以下のバケットポリシーを設定しました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Expected-network+service-principal", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::chibayuki-from-vpce/*", "Condition": { "StringEquals": { "aws:PrincipalType": [ "Account", "User", "FederatedUser", "AssumedRole", "Anonymous" ] }, "StringNotEqualsIfExists": { "aws:SourceVpc": "vpc-111bbb22" } } } ] }
ここでは Deny のステートメントのみが含まれていますが、サービスプリンシパルからの Allow を定義するステートメントは別途必要です。(マネジメントコンソールからの操作の場合、自動的に追加されます。)
aws:PrincipalType キーが取りうる値
aws:PrincipalType
はすべてのリクエストに含まれています。どういった値を取りうるかは以下ページに記載があります。
抜粋して記載すると以下の通りです。
プリンシパル | aws:PrincipalTypeの値 |
---|---|
AWS account root user | Account |
IAM user | User |
Federated user | FederatedUser |
Web federated user | AssumedRole |
SAML federated user | AssumedRole |
Assumed role | AssumedRole |
Role assigned to an Amazon EC2 instance | AssumedRole |
Anonymous caller | Anonymous |
ここにはサービスプリンシパルについての記載がないため、「上記の値以外だったらxxする」という条件づけで制御できるのでは、と考えました。(結果的には違っていたのですが、、)
期待する評価ロジック
以下のイメージです。
aws:PrincipalType
キーの値として、複数の値を指定しています。これらの値は OR 条件で評価されます。(一つでも合致すれば true 。)
先ほど確認した「取りうる値」をすべて羅列しているため、①は基本的に true 評価になることを想定しています。唯一これらの値のいずれも取らないであろう、サービスプリンシパルからのリクエストのみ false になることを期待しています。
やってみた
今回は以下の構成で試してみます。
- VPC 外からの IAM ユーザーによるアクセス
- 指定した VPC 内の VPC エンドポイント経由でのアクセス
- VPC エンドポイントを経由しない EC2 が引き受ける IAM ロールによるアクセス
- サービスプリンシパルによるアクセス
- CloudTrail(
cloudtrail.amazonaws.com
) - VPC フローログ(
delivery.logs.amazonaws.com
)
- CloudTrail(
S3 バケットには先述の Deny ステートメントを含むバケットポリシーを設定し、サービスプリンシパルからのアクセスを有効化する際に Allow ステートメントを自動で追加してもらいます。
上記で太字になっている箇所のアクセスのみ許可されるという挙動を期待しています。
VPC 外からの IAM ユーザーによるアクセス
AdministratorAccess を持つユーザーで手元の端末から aws s3 cp による PutObject を実行しました。
% aws s3 cp test.txt s3://chibayuki-from-vpce upload failed: ./test.txt to s3://chibayuki-from-vpce/test.txt An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
期待通り拒否されました。
指定した VPC 内の VPC エンドポイント経由でのアクセス
バケットポリシーで指定した VPC 上のインスタンスから実行します。インスタンスを配置したサブネットのルートテーブルでは、S3 向けのゲートウェイ型 VPC エンドポイントターゲットとするエントリを設定済みです。 aws s3 cp による PutObject を試みました。
[root@ip-192-168-0-165 ~]# touch /tmp/dummy.txt [root@ip-192-168-0-165 ~]# aws s3 cp /tmp/dummy.txt s3://chibayuki-from-vpce upload: ../tmp/dummy.txt to s3://chibayuki-from-vpce/dummy.txt
期待通り正常に実行できました。
VPC エンドポイントを経由しない EC2 が引き受ける IAM ロールによるアクセス
先ほどと同じインスタンスおよび IAM ロールを用いながら、ルートテーブルから VPC エンドポイント向けのエントリを削除した状態で試みました。ここではインターネットゲートウェイを経由してアクセスすることになります。
[root@ip-192-168-0-165 ~]# aws s3 cp /tmp/dummy.txt s3://chibayuki-from-vpce upload failed: ../tmp/dummy.txt to s3://chibayuki-from-vpce/dummy.txt An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
期待通りアクセスが拒否されました。
サービスプリンシパルによるアクセス
CloudTrail 証跡を新規作成し、出力先として S3 バケット chibayuki-from-vpce を指定します。作成時にエラーが表示され、完了しません。
証跡の作成や設定変更をマネジメントコンソールから行う際、必要なアクセス許可が与えられているかをあわせてチェックしてくれているようです。
試しに一度バケットポリシーを空にしてから証跡の設定を行い、その後 Deny ステートメントを含むバケットポリシー(必要な Allow ステートメントは含まれている)を設定し直すと、数分後に以下のような状態となります。
フローログは作成時のアクセス許可チェックは行われないようで作成自体は正常に行えますが、数分後にステータスを確認するとアクセスエラーが表示されていました。
せっかくここまで期待通りだったのに……つまづいてしまいました。
サービスプリンシパルのプリンシパルタイプは AssumedRole
試行錯誤した結果、Deny ステートメントを以下のように修正することでサービスプリンシパルからのアクセスが正常に行われるようになりました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Expected-network+service-principal", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::chibayuki-from-vpce/*", "Condition": { "StringEquals": { "aws:PrincipalType": [ "Account", "User", "FederatedUser", "Anonymous" ] }, "StringNotEqualsIfExists": { "aws:SourceVpc": "vpc-111bbb22" } } } ] }
差分としては、aws:PrincipalType
キーの値から AssumedRole を除いたのみです。
もちろんこの状態だと、サービスプリンシパル以外の AssumedRole タイプのプリンシパル(今回だとインターネットゲートウェイ経由での EC2 からのアクセス)も Deny の対象外となるため、当初のやりたいことは実現できていません。
aws:PrincipalType
キーを使用してのサービスプリンシパルのアクセス制御は実現不可能である、ということが分かってしまいました。
いいんだ、ちょっと面白かったから
当初のやりたいことは実現できませんでしたが、サービスプリンシパルもロールを引き受けた上でリクエストを実行している(であろう)、ということが分かりました。
ではどのロールを使用しているのか?というのが気になるところです。
例えば以下のページでは各サービスにおける「サービスにリンクされたロール」の有無を確認でき、CloudTrail では「あり」となっています。
詳細ページ を確認するとAWSServiceRoleForCloudTrail
という名称でサービスにリンクされたロールが作成されるとあります。しかしこれは Organizations の証跡機能を使用する際に用いられるものであり、今回試行した時点では私の環境に存在しないものです。
他にもそれらしき IAM ロールは見当たらなかったため、少なくとも自身の AWS アカウントに存在するわけではなさそうです。
となるとここから先は妄想の世界となるのですが、どこか AWS マネージドな特殊な AWS アカウントがあり、そこに専用の IAM ロールがあるのではないか、などと考えました。
実態としては IAM ロールを引き受けたセッションであるけれども、カスタマー側からは引っくるめてサービスプリンシパルとして見える、というイメージです。
このあたりは情報が見つけられなかったので完全に妄想の世界の話ですが、「もしかしたらこの世のどこかにスペシャルな IAM ロールがあるかも知れない」と考えるだけで私は幸せです。
終わりに
aws:PrincipalIsAWSService
の代わりにaws:PrincipalType
を使用してサービスプリンシパルからのアクセスの制御を試みる、という話でした。
結果的に実現できなかったのですが、サービスプリンシパルのプリンシパルタイプが AssumedRole であるという、ちょっと面白い発見ができました。
改めてトータルで考えると「そもそもどちらでもいいことを調べて、そのうえ失敗している」という生産性が無いことこの上ない行為をしていますが、有限で貴重な時間を溶かすことに快感を感じるタイプの人間なので、私は元気です。
以上、千葉(幸)がお送りしました。